﻿//-----------------------------------------------------------------------------
// FILE:	    DebugHelper.cs
// CONTRIBUTOR: Jeff Lill
// COPYRIGHT:   Copyright (c) 2021 by neonFORGE, LLC.  All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
using Neon.Common;
using Neon.Windows;
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using Task = System.Threading.Tasks.Task;

namespace RaspberryDebugger
{
    /// <summary>
    /// Remote debugger related utilties.
    /// </summary>
    internal static class DebugHelper
    {
        /// <summary>
        ///提示已安装本机Windows OpenSSH客户端  
        ///如果需要安装它的用户。  
        /// </summary>
        /// <returns><c>true</c> if OpenSSH is installed.</returns>
        public static async Task<bool> EnsureOpenSshAsync()
        {
            await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

            Log.Info("Checking for native Windows OpenSSH client");

            var openSshPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "System32", "OpenSSH", "ssh.exe");

            if (!File.Exists(openSshPath))
            {
                Log.WriteLine("Raspberry debugging requires the native Windows OpenSSH client.  See this:");
                Log.WriteLine("https://techcommunity.microsoft.com/t5/itops-talk-blog/installing-and-configuring-openssh-on-windows-server-2019/ba-p/309540");

                var button = MessageBox.Show(
                    "Raspberry debugging requires the Windows OpenSSH client.\r\n\r\nWould you like to install this now (restart required)?",
                    "Windows OpenSSH Client Required",
                    MessageBoxButtons.YesNo,
                    MessageBoxIcon.Question,
                    MessageBoxDefaultButton.Button2);

                if (button != DialogResult.Yes)
                {
                    return false;
                }

                // Install via Powershell: https://techcommunity.microsoft.com/t5/itops-talk-blog/installing-and-configuring-openssh-on-windows-server-2019/ba-p/309540

                await PackageHelper.ExecuteWithProgressAsync("Installing OpenSSH Client",
                    async () =>
                    {
                        using (var powershell = new PowerShell())
                        {
                            Log.Info("Installing OpenSSH");
                            Log.Info(powershell.Execute("Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0"));
                        }

                        await Task.CompletedTask;
                    });

                MessageBox.Show(
                    "Restart Windows to complete the OpenSSH Client installation.",
                    "Restart Required",
                    MessageBoxButtons.OK);

                return false;
            }
            else
            {
                return true;
            }
        }

        /// <summary>
        //////尝试定位要调试的启动项目，确保它是  
        /// 可以调试树莓。  
        /// </summary>
        /// <param name="dte">The IDE.</param>
        /// <returns>The target project or <c>null</c> if there isn't a startup project or it wasn't eligible.</returns>
        public static Project GetTargetProject(DTE2 dte)
        {
            ThreadHelper.ThrowIfNotOnUIThread();

            // 确定当前启动项目(如果有的话)。  

            if (dte.Solution == null)
            {
                MessageBox.Show(
                    "Please open a Visual Studio solution.",
                    "Solution Required",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Information);

                return null;
            }

            var project = PackageHelper.GetStartupProject(dte.Solution);

            if (project == null)
            {
                MessageBox.Show(
                    "请为您的解决方案选择启动项目。  ",
                    "Startup Project Required",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Information);

                return null;
            }

            //我们需要捕获相关的项目属性，而我们仍然  
            //在UI线程上，所以我们会在后台线程上有它们。  

            var projectProperties = ProjectProperties.CopyFrom(dte.Solution, project);

            if (!projectProperties.IsNetCore)
            {
                MessageBox.Show(
                    "只有.net Core 3.1或.net 5 + 6项目支持调试。  ",
                    "Invalid Project Type",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                return null;
            }

            if (!projectProperties.IsExecutable)
            {
                MessageBox.Show(
                    "只有生成可执行程序的项目类型才支持Raspberry调试。  ",
                    "Invalid Project Type",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                return null;
            }

            if (string.IsNullOrEmpty(projectProperties.SdkVersion))
            {
                MessageBox.Show(
                    "无法识别.net Core SDK版本。  ",
                    "Invalid Project Type",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                return null;
            }

            var sdkVersion = Version.Parse(projectProperties.SdkVersion);

            if (!projectProperties.IsSupportedSdkVersion)
            {
                MessageBox.Show(
                    $".NET Core SDK [{sdkVersion}] 当前不支持。 只支持。net Core版本[v3.1]或更高版本。请注意，我们目前只支持官方sdk(不是预览版或发布候选版本)，我们每一两个星期都会检查新的.net Core sdk。 如果你真的需要一个新的SDK的支持，请尽快提交一个问题:r\n\t\nhttps://github.com/nforgeio/RaspberryDebugger/issues  ",
                    "SDK Not Supported",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                return null;
            }

            if (projectProperties.AssemblyName.Contains(' '))
            {
                MessageBox.Show(
                    $"程序集名称[{projectProperties.AssemblyName}]包含空格。 这不是支持。  ",
                    "Unsupported Assembly Name",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                return null;
            }

            return project;
        }

        /// <summary>
        ///在本地构建和发布一个项目，为上传到Raspberry做准备。 这个方法  
        ///将在失败时向用户显示一个错误消息框  
        /// </summary>
        /// <param name="dte"></param>
        /// <param name="solution"></param>
        /// <param name="project"></param>
        /// <param name="projectProperties"></param>
        /// <returns></returns>
        public static async Task<bool> PublishProjectWithUIAsync(DTE2 dte, Solution solution, Project project, ProjectProperties projectProperties)
        {
            Covenant.Requires<ArgumentNullException>(dte != null, nameof(dte));
            Covenant.Requires<ArgumentNullException>(solution != null, nameof(solution));
            Covenant.Requires<ArgumentNullException>(project != null, nameof(project));
            Covenant.Requires<ArgumentNullException>(projectProperties != null, nameof(projectProperties));

            await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

            if (!await PublishProjectAsync(dte, solution, project, projectProperties))
            {
                MessageBox.Show(
                    "[dotnet publish] failed for the project.\r\n\r\nLook at the Output/Debug panel for more details.",
                    "Publish Failed",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                return false;
            }
            else
            {
                return true;
            }
        }

        /// <summary>
        /// 在本地构建和发布一个项目，为上传到Raspberry做准备。 这个方法  
        /// 失败时不向用户显示错误提示框  
        /// </summary>
        /// <param name="dte">The DTE.</param>
        /// <param name="solution">The solution.</param>
        /// <param name="project">The project.</param>
        /// <param name="projectProperties">The project properties.</param>
        /// <returns><c>true</c> on success.</returns>
        public static async Task<bool> PublishProjectAsync(DTE2 dte, Solution solution, Project project, ProjectProperties projectProperties)
        {
            Covenant.Requires<ArgumentNullException>(dte != null, nameof(dte));
            Covenant.Requires<ArgumentNullException>(solution != null, nameof(solution));
            Covenant.Requires<ArgumentNullException>(project != null, nameof(project));
            Covenant.Requires<ArgumentNullException>(projectProperties != null, nameof(projectProperties));

            // 在VS的上下文中构建项目，以确保所有的更改  
            //文件被保存，所有的依赖会首先被构建。 然后我们将  
            //验证在继续之前没有错误。  

            await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

            //确保项目完全由Visual Studio加载。 我看过  
            //当VS还在加载时构建或发布项目时随机崩溃  
            // 项目。  

            var projectGuid      = projectProperties.Guid;
            var solutionService4 = (IVsSolution4)await RaspberryDebuggerPackage.Instance.GetServiceAsync(typeof(SVsSolution));

            if (solutionService4 == null)
            {
                Covenant.Assert(solutionService4 != null, $"Service [{typeof(SVsSolution).Name}] is not available.");
            }

            // 构建项目以确保没有编译时错误。  

            Log.Info($"Building: {projectProperties.FullPath}");

            solution.SolutionBuild.BuildProject(solution.SolutionBuild.ActiveConfiguration.Name, project.UniqueName, WaitForBuildToFinish: true);

            var errorList = dte.ToolWindows.ErrorList.ErrorItems;

            if (errorList.Count > 0)
            {
                for (int i = 1; i <= errorList.Count; i++)
                {
                    var error = errorList.Item(i);

                    Log.Error($"{error.FileName}({error.Line},{error.Column}: {error.Description})");
                }

                Log.Error($"Build failed: [{errorList.Count}] errors");
                Log.Error($"See the Build/Output panel for more information");
                return false;
            }

            Log.Info("Build succeeded");

            // 发布项目，以结束所有必需的二进制文件和资产  
            //在输出文件夹。  
            //  
            //注意，我们只转发了几个标准  
            //环境变量，因为Visual Studio似乎可以交流  
            //使用环境变量和  
            //当我们调用下面的[dotnet]时，这些会导致冲突  
            //发布项目。  

            Log.Info($"Publishing: {projectProperties.FullPath}");

            await Task.Yield();

            var allowedVariableNames =
@"
ALLUSERSPROFILE
APPDATA
architecture
architecture_bits
CommonProgramFiles
CommonProgramFiles(x86)
CommonProgramW6432
COMPUTERNAME
ComSpec
DOTNETPATH
DOTNET_CLI_TELEMETRY_OPTOUT
DriverData
HOME
HOMEDRIVE
HOMEPATH
LOCALAPPDATA
NUMBER_OF_PROCESSORS
OS
Path
PATHEXT
POWERSHELL_DISTRIBUTION_CHANNEL
PROCESSOR_ARCHITECTURE
PROCESSOR_IDENTIFIER
PROCESSOR_LEVEL
PROCESSOR_REVISION
ProgramData
ProgramFiles
ProgramFiles(x86)
ProgramW6432
PUBLIC
SystemDrive
SystemRoot
TEMP
USERDOMAIN
USERDOMAIN_ROAMINGPROFILE
USERNAME
USERPROFILE
windir
";
            var allowedVariables     = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
            var environmentVariables = new Dictionary<string, string>();

            using (var reader = new StringReader(allowedVariableNames))
            {
                foreach (var line in reader.Lines())
                {
                    if (string.IsNullOrWhiteSpace(line))
                    {
                        continue;
                    }

                    allowedVariables.Add(line.Trim());
                }
            }

            foreach (string variable in Environment.GetEnvironmentVariables().Keys)
            {
                if (allowedVariables.Contains(variable))
                {
                    environmentVariables[variable] = Environment.GetEnvironmentVariable(variable);
                }
            }

            try
            {
                var response = await NeonHelper.ExecuteCaptureAsync(
                    "dotnet",
                    new object[]
                    {
                        "publish",
                        "--configuration", projectProperties.Configuration,
                        "--runtime", projectProperties.Runtime,
                        "--no-self-contained",
                        "--output", projectProperties.PublishFolder,
                        projectProperties.FullPath
                    },
                    environmentVariables: environmentVariables);

                await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

                if (response.ExitCode == 0)
                {
                    Log.Error("Publish succeeded");
                    return true;
                }

                Log.Error($"Publish failed: ExitCode={response.ExitCode}");
                Log.WriteLine(response.AllText);

                return false;
            }
            catch (Exception e)
            {
                Log.Error(NeonHelper.ExceptionError(e));
                
                return false;
            }
        }

        /// <summary>
        ///将我们从项目属性(如果有的话)中获得的调试连接名称映射到  
        ///我们的树莓连接之一。 如果没有指定名称，我们将  
        ///使用默认连接或提示用户创建连接。  
        ///如果指定了一个连接但不存在，则会显示一个错误。  
        /// </summary>
        /// <param name="projectProperties">The project properties.</param>
        /// <returns>The connection or <c>null</c> when one couldn't be located.</returns>
        public static ConnectionInfo GetDebugConnectionInfo(ProjectProperties projectProperties)
        {
            Covenant.Requires<ArgumentNullException>(projectProperties != null, nameof(projectProperties));

            var existingConnections = PackageHelper.ReadConnections();
            var connectionInfo      = (ConnectionInfo)null;

            if (string.IsNullOrEmpty(projectProperties.DebugConnectionName))
            {
                connectionInfo = existingConnections.SingleOrDefault(info => info.IsDefault);

                if (connectionInfo == null)
                {
                    if (MessageBoxEx.Show(
                        $"需要树莓连接信息。 您现在要创建连接吗?  ",
                        "Raspberry Connection Required",
                        MessageBoxButtons.YesNo,
                        MessageBoxIcon.Error,
                        MessageBoxDefaultButton.Button1) == DialogResult.No)
                    {
                        return null;
                    }

                    connectionInfo = new ConnectionInfo();

                    var connectionDialog = new ConnectionDialog(connectionInfo, edit: false, existingConnections: existingConnections);

                    if (connectionDialog.ShowDialog() == DialogResult.OK)
                    {
                        existingConnections.Add(connectionInfo);
                        PackageHelper.WriteConnections(existingConnections, disableLogging: true);
                    }
                    else
                    {
                        return null;
                    }
                }
            }
            else
            {
                connectionInfo = existingConnections.SingleOrDefault(info => info.Name.Equals(projectProperties.DebugConnectionName, StringComparison.InvariantCultureIgnoreCase));

                if (connectionInfo == null)
                {
                    MessageBoxEx.Show(
                        $"The [{projectProperties.DebugConnectionName}] Raspberry connection does not exist.\r\n\r\nPlease add the connection via:\r\n\r\nTools/Options/Raspberry Debugger",
                        "Cannot Find Raspberry Connection",
                        MessageBoxButtons.OK,
                        MessageBoxIcon.Error);

                    return null;
                }
            }

            return connectionInfo;
        }

        /// <summary>
        /// 确定项目的目标是哪个.net SDK。  
        /// </summary>
        /// <param name="projectProperties">The project properties.</param>
        /// <returns>The target <see cref="SDK"/> or <c>null</c> when one could not be located.</returns>
        public static Sdk GetTargetSdk(ProjectProperties projectProperties)
        {
            Covenant.Requires<ArgumentNullException>(projectProperties != null, nameof(projectProperties));

            // 我们将从目录返回一个匹配的SDK(如果它存在)。  
            //注意，有不止一个匹配一个独立的SDK或一个发货  
            //使用Visual Studio。 如果可能的话，我们更倾向于单机。  

            var sdkItem = PackageHelper.SdkGoodCatalog.Items.SingleOrDefault(item => item.Version == projectProperties.SdkVersion && 
                                                                                 item.IsStandalone &&
                                                                                 item.Architecture == SdkArchitecture.ARM32);
            if (sdkItem == null)
            {
                // 寻找一个Visual Studio SDK。

                sdkItem = PackageHelper.SdkGoodCatalog.Items.SingleOrDefault(item => item.Version == projectProperties.SdkVersion &&
                                                                                 item.Architecture == SdkArchitecture.ARM32);
            }

            if (sdkItem == null)
            {
                MessageBoxEx.Show(
                    $".NET Core SDK [v{projectProperties.SdkVersion}] is unknown to the Raspberry Debugger.  Please submit an issue at:\r\n\r\n{PackageHelper.GitHubIssuesUri}",
                    "Unknown .NET Core SDK",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                return null;
            }
            else
            {
                return new Sdk(sdkItem.Name, sdkItem.Version);
            }
        }

        /// <summary>
        ///建立连接到树莓，并确保树莓已经  
        ///目标SDK， <b>vsdbg</b>安装，也处理项目的上传  
        /// binaries.
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <param name="targetSdk">The target SDK.</param>
        /// <param name="projectProperties">The project properties.</param>
        /// <param name="projectSettings">The project's Raspberry debug settings.</param>
        /// <returns>The <see cref="Connection"/> or <c>null</c> if there was an error.</returns>
        public static async Task<Connection> InitializeConnectionAsync(ConnectionInfo connectionInfo, Sdk targetSdk, ProjectProperties projectProperties, ProjectSettings projectSettings)
        {
            Covenant.Requires<ArgumentNullException>(connectionInfo != null, nameof(connectionInfo));
            Covenant.Requires<ArgumentNullException>(targetSdk != null, nameof(targetSdk));
            Covenant.Requires<ArgumentNullException>(projectProperties != null, nameof(projectProperties));
            Covenant.Requires<ArgumentNullException>(projectSettings != null, nameof(projectSettings));

            var connection = await Connection.ConnectAsync(connectionInfo, projectSettings: projectSettings);

            // .NET Core only supports Raspberry models 3 and 4.
            if (!connection.PiStatus.RaspberryModel.StartsWith("Raspberry Pi 3 Model") &&
                !connection.PiStatus.RaspberryModel.StartsWith("Raspberry Pi 4 Model") &&
                !connection.PiStatus.RaspberryModel.StartsWith("Raspberry Pi Zero 2"))
            {
                await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

                MessageBoxEx.Show(
                    $"Your [{connection.PiStatus.RaspberryModel}] is not supported." +
                    $". net核心需要树莓模型3或4或Pi Zero 2。  ",
                    $"Raspberry Not Supported",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                connection.Dispose();
                return null;
            }

            //请确保已安装SDK。

            if (!await connection.InstallSdkAsync(targetSdk.Version))
            {
                await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

                MessageBoxEx.Show(
                    $"不能安装.net SDK [v{targetSdk.Version}]在Raspberry Pi。 \r\n\r\n查看调试输出以了解更多细节。  ",
                    "SDK Installation Failed",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                connection.Dispose();
                return null;
            }

            // 确保已安装调试器。

            if (!await connection.InstallDebuggerAsync())
            {
                await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

                MessageBoxEx.Show(
                    $"无法在Raspberry上安装VSDBG调试器。 \r\n\r\n查看调试输出以了解更多细节。  ",
                    "Debugger Installation Failed",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                connection.Dispose();
                return null;
            }

            // 上传程序二进制文件。

            if (!await connection.UploadProgramAsync(projectProperties.Name, projectProperties.AssemblyName, projectProperties.PublishFolder))
            {
                await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

                MessageBoxEx.Show(
                    $"无法将程序二进制文件上传到树莓。 \r\n\r\n查看调试输出以了解更多细节。  ",
                    "Debugger Installation Failed",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                connection.Dispose();
                return null;
            }

            return connection;
        }
    }
}

